一樣先簡單帶大家做一下基本的用法與架構
建立 drawer 最主要的元件,當中有許多屬性可以調整,稍後會陸續介紹
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView"
... />
menu 的部分就是幫我們注入列表式的 navigation item
<com.google.android.material.navigation.NavigationView
...
app:menu="@menu/navigation_drawer" />
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:id="@+id/group1"
android:checkableBehavior="single">
<item
android:id="@+id/group1_title"
android:title="Group 1">
<menu>
<item
android:id="@+id/item1"
android:icon="@drawable/ic_baseline_favorite_24"
android:title="item 1" />
<item
android:id="@+id/item2"
android:icon="@drawable/ic_baseline_link_24"
android:title="item 2" />
<item
android:id="@+id/item3"
android:icon="@drawable/ic_baseline_search_24"
android:title="item 3" />
</menu>
</item>
</group>
</menu>
<com.google.android.material.navigation.NavigationView
...
app:headerLayout="@layout/header_navigation_drawer" />
<LinearLayout
...
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textAppearance="?attr/textAppearanceHeadline6"
android:text="@string/header_title" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textAppearance="?attr/textAppearanceBody2"
android:textColor="@color/material_on_surface_emphasis_medium"
android:text="@string/header_text" />
</LinearLayout>
如果想在設計上做出分層分群的話,要從 menu 下手而不是 NavigationView,若對 menu 架構與寫法不熟的話,建議先看過官方文章
divider 會自動添加到具有唯一 ID 的 <group>
之間。當子 <menu>
添加到 item 時,它被視為副標題顯示
在 <group>
當中添加 android:checkableBehavior="single"
可讓選取模式變為單選,一次只能 checked 一個 item
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group
android:id="@+id/group1"
android:checkableBehavior="single">
<item
android:id="@+id/group1_title"
android:title="Group 1">
<menu>
<item
android:id="@+id/item1"
android:icon="@drawable/ic_baseline_favorite_24"
android:title="item 1" />
<item
android:id="@+id/item2"
android:icon="@drawable/ic_baseline_link_24"
android:title="item 2" />
<item
android:id="@+id/item3"
android:icon="@drawable/ic_baseline_search_24"
android:title="item 3" />
</menu>
</item>
</group>
<group
android:id="@+id/group2"
android:checkableBehavior="single">
<item
android:id="@+id/group2_title"
android:title="Group 2">
<menu>
<item
android:id="@+id/item4"
android:icon="@drawable/ic_baseline_favorite_24"
android:title="item 4" />
<item
android:id="@+id/item5"
android:icon="@drawable/ic_baseline_link_24"
android:title="item 5" />
<item
android:id="@+id/item6"
android:icon="@drawable/ic_baseline_search_24"
android:title="item 6" />
</menu>
</item>
</group>
</menu>
Modal drawer 實作上,父層級的佈局要使用 DrawerLayout 才能實現,在此佈局中的作為 drawer view 的子元件,透過設置 android:layout_gravity
來控制 drawer 的出現位置,而這邊要注意若設置 bottom 或 top 會報錯
由於 drawer 都會搭配 toolbar 一起使用,所以在 CoordinatorLayout 的部分都是關於 toolbar 的設置,如果不需要就不用特別使用。若對 toolbar 的設計實作不太懂,可以到我先前寫過的文章在複習一下
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/modal_drawer_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent"
android:elevation="0dp"
app:navigationIcon="@drawable/ic_baseline_menu_24"
app:title="Top bar" />
</com.google.android.material.appbar.AppBarLayout>
<!-- Screen content -->
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/modal_drawer_navigationView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:menu="@menu/navigation_drawer_menu"
android:background="@color/darkGrey"
app:headerLayout="@layout/itemview_drawer_header"
android:layout_gravity="start" />
</androidx.drawerlayout.widget.DrawerLayout>
官方文檔中,實作上建議搭配 status bar 的半透明化讓 drawer 能完整顯示,我的範例中沒特別用到,這邊貼給大家知道一下
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.*">
<item name="android:windowTranslucentStatus">true</item>
</style>
在為 drawer 設置 headerlayout 時,記得要再其父層佈局中加上 android:fitsSystemWindows="true"
,否則再注入到 drawer 顯示時,Top 的部分不會預留空間,導致 hearder 與 windows 離太近,如圖下
<LinearLayout
...
android:fitsSystemWindows="true">
...
</LinearLayout>
編程中,drawerLayout 有 open & close 的方法用來控制 navigationView 的展開與收起
而在 navigationView 能設置 menuItem 點擊事件的 listener,能拿到用戶當前點擊的 menuItem,在點擊後改變它的 checked 狀態,能對應之後寫的 selector 呈現選取前後的狀態改變,讓用戶知道當前是在哪一個 destination,並在之後去關閉 drawer。
這邊我簡單用一個變數去儲存之前點選過的 menuItem,讓在用戶點選新的之後,將其 checked 狀態改變,就能做到 single selected 的效果
private var oldMenuItem :MenuItem?= null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.modalDrawerToolbar.setNavigationOnClickListener {
binding.drawerLayout.open()
}
binding.modalDrawerNavigationView.setNavigationItemSelectedListener { menuItem ->
oldMenuItem?.isChecked = false
oldMenuItem = menuItem
menuItem.isChecked = true
binding.drawerLayout.close()
true
}
}
Bottom 的實作上就與 Modal 有所不同了,更像是 Bottom sheet 的做法。這邊我們總共需要兩層的 CoordinatorLayout,第一層的是用來包裹 Bottom bar 的。第二層就是重點了,包裹著一個 FrameLayout 與 NavigationView,而這邊不知道大家是否有跟我一樣的疑惑,為何要 FrameLayout?
是作為 Scrim 的,但這系統不是會幫我們實作嗎?上面的 Modal 就是如此,這是因為 Bottom 實作不是透過 DrawerLayout,就如同我一開始講的,實作上類似於 Bottom sheet,而 CoordinatorLayout 是用來協調子 View 之間動作的一個 Layout,並不會實作出 Scrim 的功能,所以我們要用一個 FrameLayout 覆蓋在我們的內容上,作為一個 Scrim
講完 FrameLayout 就回到主角身上,這邊有兩個屬性要特別設置,app:behavior_hideable="true"
將 drawer 能被收起隱藏,app:layout_behavior="@string/bottom_sheet_behavior"
將其行為設為 Bottom sheet 的模式
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize">
<!-- Screen content -->
<FrameLayout
android:id="@+id/scrim"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/bottom_drawer_navigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:behavior_hideable="true"
app:headerLayout="@layout/itemview_drawer_header"
app:layout_behavior="@string/bottom_sheet_behavior"
app:menu="@menu/navigation_drawer_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
style="@style/Widget.MaterialComponents.BottomAppBar.PrimarySurface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:navigationIcon="@drawable/ic_baseline_menu_24" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
沒有 drawer 的 open & close 能使用,我們就只能自己實現,藉由改變 BottomSheetBehavior.state,開啟時為 Expanded 收起時為 Hidden
val bottomSheetBehavior = BottomSheetBehavior.from(binding.bottomDrawerNavigationView)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
binding.bottomAppBar.setNavigationOnClickListener {
if (bottomSheetBehavior.state == BottomSheetBehavior.STATE_EXPANDED)
hiddenDrawer(bottomSheetBehavior)
else
expandDrawer(bottomSheetBehavior)
}
private fun hiddenDrawer(bottomSheetBehavior: BottomSheetBehavior<NavigationView>) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
private fun expandDrawer(bottomSheetBehavior: BottomSheetBehavior<NavigationView>) {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
item checked 的狀態變化設置,就跟我們剛剛在 Modal 實作的相同,用一個變數去儲存之前點選過的 menuItem,讓在用戶點選新的之後,將其 checked 狀態改變,就能做到 single selected 的效果
private var oldMenuItem: MenuItem? = null
binding.bottomDrawerNavigationView.setNavigationItemSelectedListener{ menuItem ->
oldMenuItem?.isChecked = false
menuItem.isChecked = true
oldMenuItem = menuItem
hiddenDrawer(bottomSheetBehavior)
true
}
這邊我們就要自己實現 scrim 的效果,功能上就是點擊之後會收起 Bottom drawer
binding.scrim.setOnClickListener {
hiddenDrawer(bottomSheetBehavior)
}
而在畫面效果上就比較麻煩了,首先要去設置 Bottom drawer 滑動的 Callback,當中要覆寫兩個功能,這邊我們只需要 onSlider,是因為我們希望隨著用戶滑動收起或展開,scrim 會跟著變化,收起時逐漸變得透明顯示主畫面;展開時逐漸變得明顯遮蓋主畫面。若是對顏色轉化或 ARGB 不太懂的可以看這篇文章
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
}
// 實現 scrim 的效果
override fun onSlide(bottomSheet: View, slideOffset: Float) {
val baseColor = Color.BLACK
val baseAlpha = ResourcesCompat.getFloat(
resources,
com.google.android.material.R.dimen.material_emphasis_medium
)
val offset = (slideOffset - (-1f)) / (1f - (-1f)) * (1f - 0f) + 0f
val alpha = MathUtils.lerp(0f, 255f, offset * baseAlpha).toInt()
val color = Color.argb(alpha, baseColor.red, baseColor.green, baseColor.blue)
binding.scrim.setBackgroundColor(color)
}
})
自訂風格上,在 Anatomy 可以看到能設置的屬性很多,除了顏色與 text 的字型外,著重在 navigaiton Item 上面,在 check 狀態變換時,呈現不同的色調來告知用戶當前是在哪個 destination
<style name="ThemeOverlay.App.NavigationView" parent="">
<!-- Container background color-->
<item name="colorSurface">@color/darkBlue</item>
<item name="textAppearanceSubtitle2">@style/TextAppearance.App.Subtitle2</item>
<item name="textAppearanceBody2">@style/TextAppearance.App.Body2</item>
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>
</style>
<style name="TextAppearance.App.Headline6" parent="TextAppearance.MaterialComponents.Subtitle1">
<item name="fontFamily">@font/alatsi</item>
<item name="android:fontFamily">@font/alatsi</item>
</style>
<style name="TextAppearance.App.Subtitle2" parent="TextAppearance.MaterialComponents.Subtitle1">
<item name="fontFamily">@font/alatsi</item>
<item name="android:fontFamily">@font/alatsi</item>
</style>
<style name="TextAppearance.App.Body2" parent="TextAppearance.MaterialComponents.Subtitle1">
<item name="fontFamily">@font/alatsi</item>
<item name="android:fontFamily">@font/alatsi</item>
</style>
<style name="Widget.App.NavigationView" parent="Widget.MaterialComponents.NavigationView">
<item name="materialThemeOverlay">@style/ThemeOverlay.App.NavigationView</item>
<item name="itemIconTint">@color/navigation_item_color</item>
<item name="itemTextColor">@color/navigation_item_color</item>
<item name="itemShapeFillColor">@color/navigation_item_background_color</item>
</style>
在 item 的設計上由於要配合 check 狀態變化,所以 color 的部分都寫成 selector 來應用,分別套用在 text 與 icon 的屬性上
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/holo_orange_light" android:state_checked="true"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface" android:state_enabled="false"/>
<item android:alpha="@dimen/material_emphasis_medium" android:color="?attr/colorOnSurface"/>
</selector>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_activated="true"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_checked="true"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorPrimary" android:state_pressed="true"/>
<item android:color="@android:color/transparent"/>
</selector>
實作上,雖然元件方面 Material Design 幾乎都幫我們包辦好了,但在佈局的設置上,例如 DrawerLayout、Coordinator 上面還是得要我們自行設定,並非直接導入 NavigationView 就能使用
而要注意的是,Modal 與 Bottom 的實現過程並不相同,Bottom 相對來說比較麻煩,要自己實現 scrim 的效果與功能,而 Modal 在使用 drawerLayout 的預設情況下就有了,建議如果只是單純想嘗試看看的,從 Modal 開始會比較好入門
若對實作還是有點不懂的,這邊提供我的 Github 方便大家參考